/**
 * This software was produced for the U. S. Government
 * under Contract No. FA8721-09-C-0002, and is
 * subject to the Rights in Noncommercial Computer Software
 * and Noncommercial Computer Software Documentation Clause
 * (DFARS) 252.227-7014 (JUN 1995)
 * 
 * (c) 2009 The MITRE Corporation. All Rights Reserved.
 *
 * SPMIF to OPNET Translator
 * Author: ewalters
 * Created: 12:09:30 PM, Sep 7, 2007
 * MITRE Corporation
 */
package com.mitre.spmif2opnet.analysis;

import java.util.Iterator;

import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;

import com.mitre.spmif2opnet.analysis.ast.CompoundNode;
import com.mitre.spmif2opnet.analysis.ast.Device;
import com.mitre.spmif2opnet.analysis.ast.ExpandedNode;
import com.mitre.spmif2opnet.analysis.ast.Graph;
import com.mitre.spmif2opnet.analysis.ast.LinkNode;
import com.mitre.spmif2opnet.analysis.ast.Node;
import com.mitre.spmif2opnet.analysis.ast.Overhead;
import com.mitre.spmif2opnet.analysis.ast.Project;
import com.mitre.spmif2opnet.analysis.ast.RepNode;
import com.mitre.spmif2opnet.analysis.ast.Scenario;
import com.mitre.spmif2opnet.analysis.ast.SynchroNode;
import com.mitre.spmif2opnet.spmif.ArcType;
import com.mitre.spmif2opnet.spmif.BasicNodeType;
import com.mitre.spmif2opnet.spmif.CPSNodeType;
import com.mitre.spmif2opnet.spmif.CompoundNodeType;
import com.mitre.spmif2opnet.spmif.DeviceType;
import com.mitre.spmif2opnet.spmif.EGType;
import com.mitre.spmif2opnet.spmif.ExpandedNodeType;
import com.mitre.spmif2opnet.spmif.LinkNodeType;
import com.mitre.spmif2opnet.spmif.OMElementType;
import com.mitre.spmif2opnet.spmif.PSType;
import com.mitre.spmif2opnet.spmif.ProcessingOverheadType;
import com.mitre.spmif2opnet.spmif.ProjectType;
import com.mitre.spmif2opnet.spmif.RepNodeType;
import com.mitre.spmif2opnet.spmif.ResourceRequirementType;
import com.mitre.spmif2opnet.spmif.SchedulingPolicyType;
import com.mitre.spmif2opnet.spmif.SynchroNodeType;

/**
 * This is an updated graph/AST builder for the new S-PMIF schema
 * This translates the Java representation of the S-PMIF into our custom AST
 */
public class GraphBuilder {
	/**
	 * Empty constructor - this is a utility class
	 */
	public GraphBuilder() { }

	/**
	 * Build the AST from the given project and context
	 *  The produced structure is stored in the project
	 * @param project the project
	 * @param c the context
	 */
	public void buildGraph(ProjectType project, AnalysisContext c) {
		Overhead ov = translate(project.getProcessingOverhead(), c);
		String file_name = c.getCurrId().getFileName();
		c.setFileLoaded(file_name);
		Project p = new Project(project.getProjectName(), ov);
		c.setProjectName(p.getName());
		
		// Construct the project graphs
		for (PSType scenario : project.getPerformanceScenario()) {
			Scenario s = translate(scenario, c);
			p.addScenario(s);
		}
		 
		c.addProject(p);
	}

	/**
	 * Translate an S-PMIF scenario to an AST one
	 * NOTE: Although it seems to be implied, is there really a 1-1
	 *  scenario - execution graph match?  The bare schema does not
	 *  necessarily enforce this!
	 * @param scenario the S-PMIF scenario
	 * @param c the context
	 * @return our AST scenario
	 */
	private Scenario translate(PSType scenario, AnalysisContext c) {
		String scen_name = scenario.getScenarioName(); 
		c.setScenarioName(scen_name);
		int num_jobs = (scenario.getNumberOfJobs() == null) ? 1 
			: scenario.getNumberOfJobs().intValue();
		int priority = (scenario.getPriority() == null) ? 1 
				: scenario.getPriority().intValue();
		String sw_filename = (scenario.getSWmodelfilename() == null) ? ""
				: scenario.getSWmodelfilename();
		Scenario s = new Scenario(scen_name,
				((EGType) scenario.getEGId()).getEGId(), // HACK
				scenario.getInterarrivalTime(),
				num_jobs,
				priority,
				sw_filename,
				scenario.getInterarrivalTimeUnits());
		c.addPriority(c.getCurrId().getProjectName(), s);
		for (EGType g : scenario.getExecutionGraph()) {
			Graph t = translate(g, c);
			s.addTask(t);
		}
		return s;
	}
	
	/**
	 * Translate an S-PMIF graph into our AST graph
	 * @param graph the S-PMIF graph
	 * @param c the context 
	 * @return our AST graph
	 */
	private Graph translate(EGType graph, AnalysisContext c) {
		String start_id = this.translateNodeId(graph.getStartNode());
		String graph_id = graph.getEGId();
		c.setGraphId(graph_id);
		String sw_filename = (graph.getSWmodelname() == null) ? ""
				: graph.getSWmodelname();
		// Caveat : we really don't use the mod date/time, so we 
		//  don't currently check to see if it is optional
		Graph t = new Graph(graph.getEGname(),
				c.getCurrId(),
				graph.isIsMainEG(),
				start_id,
				graph.getModificationDateTime(),
				sw_filename);

		Node curr_n = null;
		Iterator<JAXBElement<?>> objs = graph.getBasicNodeOrExpandedNodeOrLinkNode().iterator();
		while (objs.hasNext()) {
			JAXBElement<?> o = objs.next();
			curr_n = translateNode(o, c);
			t.addNode(curr_n); 
		}
		
		for (ArcType arc : graph.getArc()) {
			t.addEdge(translateNodeId(arc.getFromNode()), 
					  translateNodeId(arc.getToNode()));
		}

		// NOTE: perhaps we should do this in the scenario code, but here we have 
		//  a guarantee that the graph identifier is set correctly
		c.getPartitions().addId(c.getCurrId());
		return t;
	}

	/**
	 * Is this object a resource?  Needed because the JAXB classes cast
	 *  many items to straight object
	 * @param res the resource candidate
	 * @return is it a resource?
	 */
	private boolean isResource(Object res) {
		return (res instanceof ResourceRequirementType);
	}
	
	/**
	 * Extract the probability - needed because it may be null
	 * @param prob input probability
	 * @return double probability
	 */
	private double extractProbability(Float prob) {
		return (prob == null) ? 1.0 : prob.doubleValue();
	}
	
	/**
	 * Translate an S-PMIF basic node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	private Node translate(BasicNodeType n, AnalysisContext c) {
		double prob = extractProbability(n.getProbability());
		Node node = new Node(n.getNodeName(), n.getNodeId(), prob);
		if (n.getResourceRequirement().isEmpty()) {
			System.out.println("[WARNING] Basic Node " + n.getNodeName() + " " 
					+ ": No resources specified, may be ommitted!");
		}
		for (ResourceRequirementType res : n.getResourceRequirement()) {
			String res_id = translateResId(res);
			node.addResource(res_id, res.getUnitsOfService());
		}
		c.setPrevProcNode(node);
		return node;
	}

	/**
	 * Translate an S-PMIF expanded node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	private Node translate(ExpandedNodeType n, AnalysisContext c) {
		// Because it is an ExpandedSymbol, we know that it 
		//  represents a GRAPH, but we use the NODE field to store its id
		// NOTE: We don't really need to cache the graph name here, so we don't
		Identifier id = c.getCurrId();
		EGType g = null;
		String name = n.getEGname();
		try {
			g = (EGType) n.getEGId();
		} catch (ClassCastException exc) {
			System.out.println("[ERROR] Unable to infer graph from object id " + n.getEGname().toString());
			System.exit(-1);
		}
		id.setGraphId(g.getEGId());
		id.setNodeId(n.getNodeId());
		ExpandedSymbol sym = new ExpandedSymbol(id);
		double prob = extractProbability(n.getProbability());
		ExpandedNode en = new ExpandedNode(name, id.getNodeId(), sym, prob);
		c.addSymbol(sym);
		return en;
	}

	/**
	 * Translate an S-PMIF link node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	private Node translate(LinkNodeType n, AnalysisContext c) {
		Identifier id = c.getCurrId();
		id.setFileName(n.getPerformanceScenarioFilename() == null ?
				"" : n.getPerformanceScenarioFilename());
		id.setScenarioName(n.getPerformanceScenarioName());
		id.setGraphId((String) n.getEGId());
		id.setNodeId((String) n.getNodeID());
		LinkSymbol sym = new LinkSymbol(id);
		double prob = extractProbability(n.getProbability());
		LinkNode ln = new LinkNode(n.getNodeName(), id.getNodeId(), sym, prob);
		c.addSymbol(sym);
		return ln;
	}

	/**
	 * Translate an S-PMIF synchro node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	private Node translate(SynchroNodeType n, AnalysisContext c) {
		Identifier id = c.getCurrId();
		id.setNodeId(n.getNodeName());
		Identifier partner_id = new Identifier();
		partner_id.setFileName(n.getPartnerPerfFileName());
		partner_id.setScenarioName(n.getPartnerPerfScenarioName());
		EGType g = null;
		try {
			g = (EGType) n.getPartnerID();
		} catch (ClassCastException exc) {
			System.out.println("[ERROR] Unable to infer graph from object id " + n.getPartnerID().toString());
			System.exit(-1);
		}
		partner_id.setGraphId(g.getEGId());
		SyncSymbol sym = new SyncSymbol(id, n.getPartnerName(), partner_id);
		double prob = extractProbability(n.getProbability());
		SynchroNode sn = new SynchroNode(n.getNodeName(), id, sym, prob,
				n.getMyType(), n.getPartnerType());
		c.addSymbol(sym);
		return sn;
	}

	/**
	 * Translate an S-PMIF CPS node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	private Node translate(CPSNodeType n, String type_name, AnalysisContext c) {
		CompoundNode.CompoundType type = CompoundNode.fromJAXBString(type_name);
		if (type.equals(CompoundNode.CompoundType.COMPOUND_ERROR)) {
			System.out.println("[ERROR] Invalid CPSNodeType: " + type_name + "! Exiting...");
			System.exit(-1);
		}
		CompoundNode cps_node = new CompoundNode(n.getNodeName(), n.getNodeName(), type);
		Iterator<Object> objs = n.getBasicNodeOrExpandedNodeOrLinkNode().iterator();
		while (objs.hasNext()) {
			Object o = objs.next();
			Node curr_n = null;
			if (this.isResource(o)) {
				if (curr_n == null) {
					// Auto-abort if we can't tie the resource to a node
					System.out.println("[ERROR] Unable to tie resource into compound node " + cps_node.getName());
					System.exit(-1);
				} else {
					ResourceRequirementType r = (ResourceRequirementType) o; 
					String res_id = translateResId(r);
					curr_n.addResource(res_id, r.getUnitsOfService());
				}
			} else {
				// Dummy it up
				JAXBElement<?> obj = new JAXBElement(new QName(""), o.getClass(), o);  
				curr_n = translateNode(obj, c);
				cps_node.addChild(curr_n);
			}
		}
		if (c.hasPrevProcNode()) {
			cps_node.addResources(c.getPrevProcNode());
		}
		return cps_node;
	}

	// *** OBSOLETE ***
	
	// NOTE: We assume that the node only holds one of the choice properties,
	//  i.e., the compound node is not "case" and "pardo", etc.
	// NOTE: We can likely omit the resource existence checks here, since they will most
	//  likely be expressed as part of the children nodes
//	private Node translate(CompoundNodeType n, AnalysisContext c) {
//		Node out_n = null;
//		if (n.getRepetitionNode() != null) {
//			out_n = translate(n.getRepetitionNode(), c);
//		} else {
//			CPSNodeType cps_node = null;
//			CompoundNode.CompoundType ct = CompoundNode.CompoundType.COMPOUND_ERROR;
//			if (n.getCaseNode() != null) {
//				cps_node = n.getCaseNode();
//				ct = CompoundNode.CompoundType.COMPOUND_CASE;
//			} else if (n.getPardoNode() != null) {
//				cps_node = n.getPardoNode();
//				ct = CompoundNode.CompoundType.COMPOUND_PARDO;
//			} else if (n.getSplitNode() != null) {
//				cps_node = n.getSplitNode();
//				ct = CompoundNode.CompoundType.COMPOUND_SPLIT;
//			}
//			
//			// Nasty code, as we have type ambiguities, so we construct the graph via 
//			//  the following patterns:
//			// 1) Read in an element, then peek at the next one
//			// 2) If it is a resource, then add it to the current node!
//			// 3) Otherwise proceed as 1) (it is a new node)
//			if ((cps_node != null) 
//					&&  (ct != CompoundNode.CompoundType.COMPOUND_ERROR)) {
//				// For CPSNode name == id
//				CompoundNode cn = new CompoundNode(cps_node.getNodeName(), cps_node.getNodeName(), ct);
//				Node curr_n = null;
//				Iterator<Object> objs = cps_node.getBasicNodeOrExpandedNodeOrLinkNode().iterator();
//				while (objs.hasNext()) {
//					JAXBElement<?> obj = (JAXBElement<?>) objs.next();
//					Object o = obj.getValue();
//					if (this.isResource(o)) {
//						if (curr_n == null) {
//							// Auto-abort if we can't tie the resource to a node
//							System.out.println("Error: Unable to tie resource into compound node " + cps_node.getNodeName());
//							System.exit(-1);
//						} else {
//							ResourceRequirementType r = (ResourceRequirementType) o; 
//							String res_id = translateResId(r);
//							curr_n.addResource(res_id, r.getUnitsOfService());
//						}
//					} else {
//						curr_n = translateNode(obj, c);
//						cn.addChild(curr_n);
//					}
//				}
//				out_n = cn;
//			}
//		}
//		
//		for (ResourceRequirementType res : n.getResourceRequirement()) {
//			String res_id = translateResId(res);
//			out_n.addResource(res_id, res.getUnitsOfService());
//		}
//		
//		return out_n;
//	}

// *** END OBSOLETE ***
	
	/**
	 * Translate an S-PMIF rep node to our node type
	 * @param n S-PMIF node
	 * @param c context
	 * @return our node
	 */
	public Node translate(RepNodeType n, AnalysisContext c) {
		Node child = null;
		
		// Get the child type
		if (n.getBasicNode() != null) {
			child = translate(n.getBasicNode(), c);
		} else if (n.getExpandedNode() != null) {
			child = translate(n.getExpandedNode(), c);
		} else if (n.getLinkNode() != null) {
			child = translate(n.getLinkNode(), c);
		} else if (n.getSynchronizationNode() != null) {
			child = translate(n.getSynchronizationNode(), c);
		}
		
		if (child == null) {
			System.out.println("[ERROR] Empty child node for Repetition node " + n.getNodeName());
			System.exit(-1);
		}
		
		if (n.getResourceRequirement().isEmpty()) {
			System.out.println("[WARNING] Rep Node " + n.getNodeName() + " " 
					+ ": No resources specified, may be ommitted!");
		}

		for (ResourceRequirementType res : n.getResourceRequirement()) {
			child.addResource(this.translateResId(res), res.getUnitsOfService());
		}
		
		String name = (n.getNodeName() == null) ? child.getName() : n.getNodeName();
		return new RepNode(name, n.getNodeId(), n.getRepetitionFactor(), child);
	}

	/**
	 * Translate an S-PMIF overhead to our representation
	 * @param overhead S-PMIF node
	 * @param c context
	 * @return our overhead
	 */
	private Overhead translate(ProcessingOverheadType overhead, AnalysisContext c) {
		Overhead ov = new Overhead();
		// Overhead can be optional
		if (overhead == null) return ov;
		// Build the Devices
		for (DeviceType dt : overhead.getDevices().getDevice()) {
			Device d = translate(dt, c);
			ov.addDevice(d);
		}
		// Build the overheadmatrix
		// We also fold in resource construction here, since we only care about resources
		//  hooked up to devices
		for (OMElementType om : overhead.getOverheadMatrix().getOMElement()) {
			try {
				ProcessingOverheadType.SoftwareResources.SoftwareResource res = 
					(ProcessingOverheadType.SoftwareResources.SoftwareResource) om.getResourceID();
				DeviceType dt = (DeviceType) om.getDeviceID();
				ov.addOverhead(res.getResourceID(),
						res.getResourceName(),
						om.getAmountOfService(),
						dt.getDeviceID());
			} catch (ClassCastException exc) {				
				System.out.println("[ERROR] Unexpected classes located in overhead! - " + exc.getMessage());
				System.exit(-1);
			}
		}
		
		// Validate the resources - as a precaution
		for (ProcessingOverheadType.SoftwareResources.SoftwareResource res : overhead.getSoftwareResources().getSoftwareResource()) {
			if (!ov.resourceExists(res.getResourceID())) {
				System.out.println("[ERROR] Invalid resource " + res.getResourceID() + " located in overhead!");
				System.exit(-1);
			}
		}
		
		return ov;
	}

	/**
	 * Translate an S-PMIF device to our node type
	 * @param device S-PMIF element
	 * @param c context
	 * @return our device
	 */
	public Device translate(DeviceType device, AnalysisContext c) {
		
		// If we didn't set the priority from the command-line, do so now
		SchedulingPolicyType spt = device.getSchedulingPolicy();
		Priorities.PriorityType ptype = null; 
		if (spt != null) {
			ptype = Priorities.PriorityType.fromString(spt.toString());
		}
		if (c.getPriorities() == null) {
			if (ptype == null) {
				ptype = Priorities.PriorityType.DIRECT_PRIORITY;
			}
			c.setPriorities(ptype.makePriorities());
		} else {  // Double check that we maintain the same priority scheme
			if (ptype != null) {
				int order = ptype.compareTo(c.getPriorities().getPriorityType());
				if (order < 0) {
					System.out.println("[WARNING] Existing Priority Type " + c.getPriorities().getPriorityType().toString() 
							+ " overridden by newer priority " + ptype.toString() + "!");
					c.setPriorities(ptype.makePriorities());
				} else if (order > 0){
					System.out.println("[WARNING] Existing Priority Type " + c.getPriorities().getPriorityType().toString() 
							+ " conflicts with newer priority " + ptype.toString() + "!  Skipping...");
				}
			} 
		}
		
		return new Device(device.getDeviceName(),
				device.getDeviceID(),
				device.getQuantity().intValue(),
				device.getDeviceFeature(),
				device.getSchedulingPolicy(),
				device.getServiceUnits(),
				device.getServiceTime(),
				c.getProps());
	}
	
	// The following functions are unfortunately unavoidable since we are forced to
	//  deal with "Objects" from the JAXB-derived code
	// We also need to explicitly suffix the translate functions since all the 
	//  args come from "Object", i.e. overloading becomes broken
	
	/**
	 * Translate a resource requirement to its string id
	 * @param res the resource
	 * @return the id
	 */
	private String translateResId(ResourceRequirementType res) {
		ProcessingOverheadType.SoftwareResources.SoftwareResource r = null;
		try {
			r = (ProcessingOverheadType.SoftwareResources.SoftwareResource) res.getResourceId();
		} catch (ClassCastException exc) {
			System.out.println("[ERROR] Unable to infer resource name from object id " + r.toString());
			System.exit(-1);						
		}
		return r.getResourceID();
	}

	
	/**
	 * Master code for translating a JAXB object to a node
	 * NOTE: RepNodeType may have been completely detached from compound nodes
	 * @param obj the object
	 * @param c the context
	 * @return our node
	 */
	private Node translateNode(JAXBElement<?> obj, AnalysisContext c) {
		Node n = null;
		Object o = obj.getValue();
		if (o instanceof BasicNodeType) {
			n = translate((BasicNodeType) o, c);
		} else if (o instanceof ExpandedNodeType) {
			n = translate((ExpandedNodeType) o, c);
		} else if (o instanceof LinkNodeType) {
			n = translate((LinkNodeType) o, c);
		} else if (o instanceof SynchroNodeType) {
			n = translate((SynchroNodeType) o, c);
		} else if (o instanceof CPSNodeType) {
			n = translate((CPSNodeType) o, obj.getName().toString(), c);
		} else if (o instanceof RepNodeType) {
			n = translate((RepNodeType) o, c);
		} else {
			System.out.println("[ERROR] Invalid node type " + o.toString() 
					+ " encountered while building graph!");
			System.exit(-1);
		}
		return n;
	}
	
	/**
	 * Get the S-PMIF node id for the node we are translating
	 * @param o the input node
	 * @return the od
	 */
	private String translateNodeId(Object o) {
		String id = null;
		if (o instanceof BasicNodeType) {
			BasicNodeType n = (BasicNodeType) o;
			id = n.getNodeId();
		} else if (o instanceof ExpandedNodeType) {
			ExpandedNodeType n = (ExpandedNodeType) o;
			id = n.getNodeId();
		} else if (o instanceof LinkNodeType) {
			LinkNodeType n = (LinkNodeType) o;
			id = n.getNodeID();
		} else if (o instanceof SynchroNodeType) {
			SynchroNodeType n = (SynchroNodeType) o;
			id = n.getNodeName();
		} else if (o instanceof CPSNodeType) {
			CPSNodeType n = (CPSNodeType) o;
			id = n.getNodeName();
		} else if (o instanceof CompoundNodeType) {
			CompoundNodeType n = (CompoundNodeType) o;
			if (n.getRepetitionNode() != null) {
				id = n.getRepetitionNode().getNodeId();
			} else if (n.getCaseNode() != null) {
				id = n.getCaseNode().getNodeName();
			} else if (n.getPardoNode() != null) {
				id = n.getPardoNode().getNodeName();
			} else if (n.getSplitNode() != null) {
				id = n.getSplitNode().getNodeName();
			}
		} else if (o instanceof RepNodeType) {
			RepNodeType n = (RepNodeType) o;
			id = n.getNodeId();
		} else if (o instanceof EGType) { // New expanded node!
			EGType n = (EGType) o;
			id = n.getEGId();
		} else {
			System.out.println("[ERROR] Invalid node type " + o.toString() 
					+ " encountered while translating node name!");
			System.exit(-1);
		}
		
		return id;	
	}	
}